<template>
  <view class="s-upload" :class="custom_class" :style="custom_style">
    <s-grid
      v-if="!custom"
      :square="square"
      :border="false"
      :column="column"
      :gutter="gutter"
    >
      <!-- 预览样式 -->
      <template v-if="showFileList">
        <s-grid-item
          v-for="(item, index) of list"
          :key="index"
          hover-class="none"
          :width="width"
          :height="height"
          :radius="radius"
        >
          <view class="s-upload__preview" @click="onPreviewItem(index)">
            <s-image
              v-if="item.isImage"
              custom-class="s-upload__preview-image"
              :mode="imageFit"
              :src="item.thumb || item.url"
            />
            <template v-else-if="item.isVideo">
              <video
                class="s-upload__preview-video"
                :controls="false"
                :show-center-play-btn="false"
                :object-fit="videoFit"
                :src="item.url"
                :poster="item.thumb"
              />
              <view v-if="item.status === 'success'" class="s-upload__play">
                <s-icon name="play-circle" />
              </view>
            </template>
            <view v-else class="s-upload__file">
              <s-icon name="description" custom-class="s-upload__file-icon" />
              <view class="s-upload__file-name s-ellipsis">
                {{ item.name || item.url }}
              </view>
            </view>
            <view
              v-if="item.status === 'uploading' || item.status === 'failed'"
              class="s-upload__mask"
            >
              <s-icon
                v-if="item.status === 'failed'"
                name="close"
                custom-class="s-upload__mask-icon"
              />
              <s-loading v-else custom-class="s-upload__mask-icon" />
              <text v-if="item.message" class="s-upload__mask-message">
                {{ item.message }}
              </text>
            </view>
            <view
              v-if="deletable && item.deletable"
              class="s-upload__preview-delete"
              :style="delete_btn_style"
              @click.stop="onDeleteItem(index)"
            >
              <s-icon name="cross" custom-class="s-upload__preview-delete-icon" />
            </view>
          </view>
        </s-grid-item>
      </template>
      <!-- 上传按钮 -->
      <s-grid-item
        v-if="show_upload"
        hover-class="none"
        :width="width"
        :height="height"
        :radius="radius"
      >
        <view class="s-upload__slot" @click.stop="startUpload">
          <slot name="trigger">
            <view
              class="s-upload__upload"
              :class="{ 's-upload__upload--disabled': disabled }"
              :style="upload_style"
            >
              <s-icon
                :name="uploadIcon"
                :size="uploadIconSize"
                :color="uploadIconColor"
                custom-class="s-upload__upload-icon"
              />
              <text
                v-if="uploadText"
                class="s-upload__upload-text"
                :style="upload_text_style"
              >
                {{ uploadText }}
              </text>
            </view>
          </slot>
        </view>
      </s-grid-item>
    </s-grid>
    <slot :scopeParams="scopeParams" :list="list" :showUpload="show_upload" />
  </view>
</template>

<script>
import componentMixin from '../../mixins/componentMixin';
import isPromise from '../../utils/isPromise';
import showToast from '../../utils/showToast';
import { chooseFile, formatSize, isImageFile, isVideoFile } from './utils';

/**
 * s-upload 文件上传
 * @description 用于将本地的图片或文件上传至服务器，并在上传过程中展示预览图和上传进度。目前 Upload 组件不包含将文件上传至服务器的接口逻辑，该步骤需要自行实现。
 * @property {Boolean} custom 是否自定义显示列表和上传按钮，可通过ref触发上传方法
 * @property {String} name 标识符，可以在回调函数的第二项参数中获取
 * @property {String} accept = [image|media|file|video|all] 接受的文件类型
 * @property {Array} fileList 上传的文件数据
 * @property {Boolean} disabled 是否禁用文件上传
 * @property {Boolean} multiple 是否开启图片多选，部分安卓机型不支持
 * @property {Number|String} column 一行显示几个已上传内容
 * @property {Number|String} gutter 已上传内容之间间隔
 * @property {Number|String} width 上传容器和已上传内容宽度
 * @property {Number|String} height 上传容器和已上传内容高度
 * @property {Boolean} square 是否根据宽度适配上传容器高度为正方形
 * @property {Number} maxSize 文件大小限制，单位为byte
 * @property {Number} maxCount 文件上传数量限制
 * @property {Boolean} deletable 是否展示删除按钮
 * @property {String|Object} deleteBtnStyle 删除按钮样式
 * @property {Boolean} showUpload 是否展示文件上传按钮
 * @property {Boolean} showFileList 是否在上传完成后展示预览图
 * @property {String} imageFit 预览图裁剪模式，可选值参考小程序image组件的mode属性
 * @property {String} videoFit 可参考视频video组件object-fit选项
 * @property {String} background 上传按钮背景
 * @property {String|Object} uploadStyle 上传按钮样式
 * @property {String} uploadText 上传区域提示文字
 * @property {String|Object} uploadTextStyle 上传区域提示文字样式
 * @property {String} uploadIcon 上传区域图标，可选值见 Icon 组件
 * @property {Number|String} uploadIconSize 上传区域图标大小
 * @property {String} uploadIconColor 上传区域图标颜色
 * @property {Array<String>} sizeType [original,compressed] 所选的图片的尺寸, 当accept为image类型时设置所选图片的尺寸可选值为original compressed
 * @property {Array<String>} sourceType [album,camera] 图片或者视频选取模式，当accept为image类型时设置sourceType可选值为camera可以直接调起摄像头
 * @property {Array<String>} mediaType [image,video,mix] 只有在微信小程序端，当accept为media类型时有效
 * @property {Boolean} compressed 当 accept 为 video 时生效，是否压缩视频，默认为false
 * @property {Number} maxDuration 当 accept 为 video 时生效，拍摄视频最长拍摄时间，单位秒
 * @property {String} camera [back,front] 当 accept 为 video 时生效，可选值为 back front
 * @property {Boolean} previewFullImage 是否在点击后预览图片
 * @property {Boolean} previewFullVideo 是否在点击后预览视频
 * @property {Boolean} previewFullFile 是否在点击后预览除图片和视频的其它文件
 * @property {Boolean} useBeforeRead 是否开启文件读取前事件
 * @property {Function} beforeRead 文件读取前，在回调函数中返回 false 可终止文件读取
 * @property {Function} afterRead 文件读取完成后，可在此处上传
 * @event {Function} before-read ({file,name,index}) 绑定事件的同时需要将use-before-read属性设置为true
 * @event {Function} after-read ({file,name,index}) 文件读取完成后
 * @event {Function} oversize ({file,name,index}) 文件超出大小限制
 * @event {Function} delete ({file,name,index}) 删除文件
 * @event {Function} preview ({file,name,index}) 点击预览时触发
 * @event {Function} preview-image ({file,name,index}) 点击预览图片时触发
 * @event {Function} preview-video ({file,name,index}) 点击预览视频时触发
 * @event {Function} preview-file ({file,name,index}) 点击预览文件时触发
 * @example <s-upload :file-list="fileList" :after-read="afterRead" @delete="remove"/>
 */
export default {
  name: 'SUpload',
  mixins: [componentMixin],
  props: {
    custom: Boolean,
    name: {
      type: String,
      default: '',
    },
    accept: {
      type: String,
      default: 'image',
    },
    fileList: {
      type: Array,
      default: () => [],
    },
    disabled: Boolean,
    multiple: Boolean,
    column: {
      type: [Number, String],
      default: 3,
    },
    gutter: {
      type: [Number, String],
      default: 20,
    },
    width: [Number, String],
    height: [Number, String],
    background: String,
    radius: [Number, String],
    square: Boolean,
    maxSize: {
      type: Number,
      default: Number.MAX_VALUE,
    },
    maxCount: {
      type: Number,
      default: 100,
    },
    deletable: {
      type: Boolean,
      default: true,
    },
    deleteBtnStyle: [String, Object],
    showUpload: {
      type: Boolean,
      default: true,
    },
    showFileList: {
      type: Boolean,
      default: true,
    },
    imageFit: {
      type: String,
      default: 'aspectFill',
    },
    videoFit: {
      type: String,
      default: 'cover',
    },
    uploadStyle: [String, Object],
    uploadText: String,
    uploadTextStyle: [String, Object],
    uploadIcon: {
      type: String,
      default: 'plus',
    },
    uploadIconSize: [Number, String],
    uploadIconColor: String,
    mediaType: {
      type: Array,
      default: () => ['image', 'video'],
    },
    sourceType: {
      type: Array,
      default: () => ['album', 'camera'],
    },
    sizeType: {
      type: Array,
      default: () => ['original', 'compressed'],
    },
    compressed: {
      type: Boolean,
      default: false,
    },
    maxDuration: {
      type: Number,
      default: 60,
    },
    camera: {
      type: String,
      default: 'back',
    },
    previewFullImage: {
      type: Boolean,
      default: true,
    },
    previewFullVideo: {
      type: Boolean,
      default: true,
    },
    previewFullFile: {
      type: Boolean,
      default: true,
    },
    useBeforeRead: Boolean,
    beforeRead: Function,
    afterRead: Function,
  },
  data() {
    return {
      list: [],
    };
  },
  computed: {
    upload_style() {
      return this.$mergeStyle({
        background: this.background,
      }, this.uploadStyle);
    },
    upload_text_style() {
      return this.$mergeStyle(this.uploadTextStyle);
    },
    delete_btn_style() {
      return this.$mergeStyle(this.deleteBtnStyle);
    },
    show_upload() {
      return this.list.length < this.maxCount && this.showUpload;
    },
  },
  watch: {
    fileList: {
      immediate: true,
      deep: true,
      handler() {
        this.formatFileList();
      },
    },
  },
  methods: {
    formatFileList() {
      this.list = this.fileList.map(item => ({
        ...item,
        isImage: isImageFile(item),
        isVideo: isVideoFile(item),
        deletable: typeof item.deletable === 'boolean' ? item.deletable : true,
      }));
    },
    getDetail(index) {
      return {
        name: this.name,
        index: typeof index === 'number' ? index : this.fileList.length,
      };
    },
    startUpload() {
      const { maxCount, multiple, list, disabled } = this;
      if (disabled) return;
      chooseFile({
        accept: this.accept,
        multiple: this.multiple,
        sourceType: this.sourceType,
        mediaType: this.mediaType,
        compressed: this.compressed,
        maxDuration: this.maxDuration,
        sizeType: this.sizeType,
        camera: this.camera,
        maxCount: maxCount - list.length,
      }).then(res => {
        this.onBeforeRead(multiple ? res : res[0]);
      }).catch(error => {
        this.$emit('error', error);
      });
    },
    onBeforeRead(file) {
      const beforeRead = this.$getPropsFn('beforeRead');
      const useBeforeRead = this.useBeforeRead;
      let res = true;
      if (beforeRead) {
        res = beforeRead({ file, ...this.getDetail() });
      }
      if (useBeforeRead) {
        res = new Promise((resolve, reject) => {
          this.$emit('before-read', {
            file,
            ...this.getDetail(),
            callback: ok => ok ? resolve() : reject(),
          });
        });
      }
      if (!res) return;
      if (isPromise(res)) {
        res.then(data => this.onAfterRead(data || file));
      } else {
        this.onAfterRead(file);
      }
    },
    onAfterRead(file) {
      const { maxSize } = this;
      const afterRead = this.$getPropsFn('afterRead');
      const oversize = Array.isArray(file)
        ? file.some(item => item.size > maxSize)
        : file.size > maxSize;
      const options = { file, ...this.getDetail() };
      if (oversize) {
        showToast(`文件大小不能超过 ${formatSize(maxSize)}`);
        this.$emit('oversize', options);
        return;
      }
      if (afterRead) afterRead(options);
      this.$emit('after-read', options);
    },
    onDeleteItem(index) {
      this.$emit('delete', {
        file: this.list[index],
        ...this.getDetail(index),
      });
    },
    onPreviewItem(index) {
      const file = this.list[index];
      const options = { file, ...this.getDetail(index) };
      this.$emit('preview', options);
      if (file.isImage) {
        this.$emit('preview-image', options);
        this.previewFullImage && this.previewImage(index);
      } else if (file.isVideo) {
        this.$emit('preview-video', options);
        this.previewFullVideo && this.previewVideo(index);
      } else {
        this.$emit('preview-file', options);
        this.previewFullFile && this.previewFile(index);
      }
    },
    previewImage(index) {
      const list = this.list.filter(item => isImageFile(item));
      uni.previewImage({
        urls: list.map(item => item.url),
        current: list.indexOf(this.list[index]),
        fail() {
          showToast('预览图片失败');
        },
      });
    },
    previewVideo(index) {
      const list = this.list.filter(item => isVideoFile(item));
      uni.previewMedia({
        sources: list.map(item => ({ ...item, type: 'video' })),
        current: list.indexOf(this.list[index]),
        fail() {
          showToast('预览视频失败');
        },
      });
    },
    previewFile(index) {
      uni.downloadFile({
        url: this.list[index].url,
        success(res) {
          uni.openDocument({
            filePath: res.tempFilePath,
            showMenu: true,
          });
        },
      });
    },
  },
};
</script>

<style lang="scss" src="./index.scss"></style>
